<?php

/*
Quad_Date
Copyright (c) 2009 Ken Snyder
http://code.google.com/p/quadphp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

date_default_timezone_set(date_default_timezone_get());

class Quad_Date {
	
	// copy of DateTime constants
	const ATOM   = 'Y-m-d\TH:i:sP';
	const COOKIE = 'l, d-M-y H:i:s T';
	const ISO8601 = 'Y-m-d\TH:i:sO';
	const RFC822 = 'D, d M y H:i:s O';
	const RFC850 = 'l, d-M-y H:i:s T';
	const RFC1036 = 'D, d M y H:i:s O';
	const RFC1123 = 'D, d M Y H:i:s O';
	const RFC2822 = 'D, d M Y H:i:s O';
	const RFC3339 = 'Y-m-d\TH:i:sP';
	const RSS = 'D, d M Y H:i:s O';
	const W3C = 'Y-m-d\TH:i:sP';
	
	public $raw;
	public $isValid = false;
	protected $_timezone;
	protected $_datestamp;
	
	public function __construct($date = null, $zoneOrOffset = null) {
		$args = func_get_args();
		call_user_func_array(array($this,'setDate'), $args);		
	}
	
	public function __sleep() {
		$this->_datestamp = $this->format(self::ISO8601);
		// TODO: serialize timezone info
		return array('_datestamp');
	}
	
	public function __wakeup() {
		$this->setDate($this->_datestamp);
		unset($this->_datestamp);
	}	
	
	public function isValid() {
		return $this->isValid;
	}
	
	public function setYear($Y) {
		$this->setDate($Y . $this->format("-m-d H:i:s"));
		return $this;
	}
	
	public function setMonth($m) {
		$this->setDate($this->format("Y-") . $m . $this->format("-d H:i:s"));
		return $this;
	}
	
	public function setDay($d) {
//Quad_Debug::dump((string) $this, $this->format("Y-m-") . $d . $this->format(" H:i:s"));		
		$this->setDate($this->format("Y-m-") . $d . $this->format(" H:i:s"));
		return $this;
	}
	
	public function setHour($H) {
		$this->setDate($this->format("Y-m-d ") . $H . $this->format(":i:s"));
		return $this;
	}
	
	public function setMinute($i) {
		$this->setDate($this->format("Y-m-d H:") . $i . $this->format(":s"));
		return $this;
	}
	
	public function setSecond($s) {
		$this->setDate($this->format("Y-m-d H:i:") . $s);
		return $this;
	}
	
	public function getYear() {
		return (int) $this->format('Y');
	}

	public function getMonth() {
		return (int) $this->format('n');
	}
	
	public function getDay() {
		return (int) $this->format('j');
	}	
	
	public function getHour() {
		return (int) $this->format('H');
	}	
	
	public function getMinute() {
		return (int) $this->format('i');
	}
	
	public function getSecond() {
		return (int) $this->format('s');
	}

	public function getDayOfWeek() {
		return (int) $this->format('w');
	}
	
	public function isWeekday() {
		$dow = $this->getDayOfWeek();
		return ($dow >= 1 && $dow <= 5);
	}
	
	public function isWeekend() {
		return !$this->isWeekend();
	}
	
//	public function isBankHoliday() {
//		
//	}
	
	public function setToLastDayOfMonth() {
		$this->setDay($this->getDaysInMonth());
		return $this;
	}
	
	public function setToFirstDayOfMonth() {
//Quad_Debug::dump('setToFirstDayOfMonth', (string) $this);		
		$this->setDay(1);
		return $this;
	}
	
	public function setTimestamp($ts = null) {
    $this->raw->__construct($ts ? "@$ts" : null, $this->raw->getTimezone());
    return $this;
	}
	
	public function getTimestamp() {
		return (float) $this->format('U');
	}
	
	/**
	 * supported signatures:
	 * setDate($year, $month, $day)
	 * setDate($year, $month, $day, $hour, $minute, $second, $zoneOrOffset=null)
	 * setDate()
	 * setDate($unix, $zoneOrOffset=null)
	 * setDate(Quad_Date $date, $zoneOrOffset=null)
	 * setDate($dateString, $zoneOrOffset=null)
	 */	
	public function setDate($date = null, $zoneOrOffset = null) {
		$this->isValid = true;
		$this->setTimezone($zoneOrOffset);
		
		if (func_num_args() > 2) {
			$parts = array();
			// populate default date parts
			$parts = array(null,null,"01","00","00","00",null);
			foreach (func_get_args() as $i => $a) {
				$parts[$i] = $a;
			}
			$zoneOrOffset = array_pop($parts);
			$date = $parts[0] . '-' . $parts[1] . '-' . $parts[2] . ' ' .
				$parts[3] . ':' . $parts[4] . ':' . $parts[5];
		}
		
		if (is_int($date) || is_float($date) || $date === null) {
			$this->raw = new DateTime();
			$this->setTimestamp($date);
			return $this;		
		}
		// try to get new date and time
		try {
			$date = $this->_prefilterDateString($date);
			$raw = new DateTime($date);
		}
		catch (Exception $e) {
			// Quad_Debug::dump("DateTime Exception from creation param `$date`", $e->getMessage());			
			$raw = new DateTime('0000-00-00');
		}
		$this->isValid = ($raw->format('Y') != '-0001');		
		$this->raw = $raw;
		return $this;
	}
	
	protected function _prefilterDateString($date) {
		$date = preg_replace('/^(0?\d|1[0-2])-([0-2]?\d|3[01])-(\d{4})/','$3-$1-$2',$date);
		return $date;
	}
	
	public function setTime($H,$i,$s) {
		$H = (int) $H;
		$i = (int) $i;
		$s = (int) $s;
		if ($H < 10) {
			$H = '0' . $H;
		}
		if ($i < 10) {
			$i = '0' . $i;
		}
		if ($s < 10) {
			$s = '0' . $s;
		}				
		$newStr = $this->format("Y-m-d") . " $H:$i:$s";
		$this->setDate($newStr);
		return $this;
	}
	
	//
	public function setTimezone($zoneOrOffset) {
		$oTimeZone = false;
		// string time offset such as -0600 or +3:00
		if (is_string($zoneOrOffset) && preg_match('/^([+-]?)([0-2]?\d):?([0-5]\d)$/', $zoneOrOffset, $match)) {
			list (, $sign, $hours, $minutes) = $match;
			$mult = ($sign == '-' ? -1 : 1);
			$zoneOrOffset = $mult * ((int) $hours * 60 + (int) $minutes) * 60;
//die("{$match[0]} -- $zoneOrOffset");						
		}
		if ($zoneOrOffset instanceof DateTimeZone) {
			$oTimeZone = $zoneOrOffset;
		}
		elseif (is_int($zoneOrOffset) || is_string($zoneOrOffset) || is_float($zoneOrOffset)) {
			try {
				// integer offset
				if (is_int($zoneOrOffset)) {
					$abbr = DateTimeZone::listAbbreviations();
					foreach ($abbr as $name => $zones) {
						if ((int) $zones[0]['offset'] == $zoneOrOffset) {
							$zone = $zones[0]['timezone_id'];
//die("$zoneOrOffset -- $zone");							
							$oTimeZone = new DateTimeZone($zone);
							break;
						}
					}
				}
				// abbreviation such as EST or GMT
				elseif (preg_match('/^[a-z]+$/i', $zoneOrOffset)) {
					$zoneOrOffset = strtolower($zoneOrOffset);
					$abbr = DateTimeZone::listAbbreviations();
					if (isset($abbr[$zoneOrOffset])) {
						$zone = $abbr[$zoneOrOffset][0]['timezone_id'];
						$oTimeZone = new DateTimeZone($zone);
					}
				}
				// normal zone name for DateTimeZone such as America/Denver
				elseif (strpos($zoneOrOffset,'/') > 1) {
					$oTimeZone = new DateTimeZone($zoneOrOffset);
				}
			}
			catch (Exception $e) {
				// tz is invalid
				$oTimeZone = false;
			}
		}
		if ($oTimeZone instanceof DateTimeZone) {
			$this->raw->setTimeZone($oTimeZone);
			$this->_timezone = $oTimeZone;
		}
		return $this;
	}
	
	public function getDaysInMonth() {
		list($year, $month) = explode(' ', $this->raw->format('Y n'));
		return self::daysInMonth($year, $month);
	}
	
	public static function daysInMonth($year, $month) {
		$month = (int) $month;
		if ($month == 2) {
			$test = new DateTime("$year-2-29");
			return ($test->format('j') == '29' ? 29 : 28);
		}
		static $map = array(31,null,31,30,31,30,31,31,30,31,30,31);
		return $map[$month-1];
	}	
	
	public static function checkdate($y,$m,$d) {
		$y = (int) $y;
		$m = (int) $m;
		$d = (int) $d;
		if (
			($y < -9999) ||
			($y > 9999) ||
			($y == 0) ||
			($m < 1) ||
			($m > 12) ||
			($d < 1) ||
			($d > 31) ||
			($d > 28 && $d > self::daysInMonth($y,$m))
		) {
			return false;
		}
		return true;
	}
	
	public static function create($date = null, $zoneOrOffset = null) {
		if ($date instanceof self) {
			$date = clone $date;
			if ($zoneOrOffset instanceof DateTimeZone) {
				$date->setTimezone(clone $zoneOrOffset);
			}
			return $date;
		}
		$a = func_get_args();
		switch (count($a)) {
			case 0:
				return new self();
			case 1:
				return new self($a[0]);
			case 2:
				return new self($a[0],$a[1]);
			case 3:
				return new self($a[0],$a[1],$a[2]);
			case 4:
				return new self($a[0],$a[1],$a[2],$a[3]);
			case 5:
				return new self($a[0],$a[1],$a[2],$a[3],$a[4]);
			case 6:
				return new self($a[0],$a[1],$a[2],$a[3],$a[4],$a[5]);
			case 7:
			default:
				return new self($a[0],$a[1],$a[2],$a[3],$a[4],$a[5],$a[6]);
		}
	}
	
	public function __call($m, $a) {
		$call = array($this->raw, $m);
		if (!is_callable($call)) {
			throw new Exception("Quad_Date::$m() is not defined.");
		}
		return call_user_func_array($call, $a);
	}
	
	public function isBetween($d1, $d2, $inclusive = true) {
		$d1 = self::create($d1);
		$d2 = self::create($d2);
		if ($inclusive) {
			return @($this->raw >= $d1 && $this->raw <= $d2);
		}
		return @($this->raw > $d1 && $this->raw < $d2);
	}
	
	public function isBefore($d1) {
		$d1 = self::create($d1);
		return ($this->raw < $d1->raw);
	}
	
	public function isLessThan($d1) {
		$d1 = self::create($d1);
		return ($this->raw < $d1->raw);
	}
	
	public function isAfter($d1) {
		$d1 = self::create($d1);
		return ($this->raw > $d1->raw);
	}
	
	public function isGreaterThan($d1) {
		$d1 = self::create($d1);
		return ($this->raw > $d1->raw);
	}
	
	public function equals($d1) {
		$d1 = self::create($d1);
		return ($this->raw == $d1->raw);
	}
	
	// TODO: accept multiple args
	public function min($d1) {
		$d1 = self::create($d1);
		if ($d1->raw > $this->raw) {
			return $this;
		}
		return $d1;
	}
	
	public function max($d1) {
		$d1 = self::create($d1);
		if ($d1->raw < $this->raw) {
			return $this;
		}
		return $d1;
	}
	
	public function diff($date, $absolute = false) {
		$dates = array(Quad_Date::create($date), $this);
		$interval = new Quad_Date_Interval($dates, $absolute);
		return $interval;
	}
	
	public function add($amount) {
		$new = clone $this;
		$new->raw->modify($amount);
		return $new;
	}
	
	public function sub($amount) {
		$amount = trim($amount);
		if (substr($amount,0,1) == '-') {
			$amount = '+' . substr($amount,1);
		}
		if (substr($amount,0,1) == '+') {
			$amount = '-' . substr($amount,1);
		}
		else {
			$amount = '-' . $amount;
		}
		return $this->add($amount);
	}
	
	public function modify($amount) {
		$this->raw->modify($amount);
		return $this; // null is returned before php 5.3
	}
	
	public function format($code = null) {
		if ($code === false || $code === null) {
			return $this;
		}
		if (!$this->isValid) {
			return '';
		}		
		if (strpos($code, '%') !== false) {
			$code = preg_replace_callback('/%#?[a-z]/i', array('self','pregReplaceFormat'), $code);
		}		
		$formatted = $this->raw->format($code);
		if (strpos($code,'M.') !== false || strpos($code,'%b.') !== false) {
			$formatted = str_replace('May.', 'May', $formatted);
		}
		return $formatted;
	}
	
	public function __toString() {
		return $this->raw->format('Y-m-d H:i:s');
	}
	
	public function __clone() {
		$this->raw = clone $this->raw;
		if ($this->_timezone) {
			$this->raw->setTimezone(clone $this->_timezone);
		}
	}
	
	// chainable cloning
	public function copy() {
		return clone $this;
	}
	
	public static function pregReplaceFormat($letter) {
		// convert % style codes to php style codes
		static $map = array(
			// year
			'%Y'=>'Y', // 4 digit year
			'%y'=>'y', // 2 digit year
			// month
			'%m'=>'m', // 2 digit month
			'%#m'=>'n', // 1 digit month
			'%b'=>'M', // Abbr. Month name
			'%B'=>'F', // Full month name
			// day
			'%d'=>'d', // 2 digit day
			'%#d'=>'j', // 1 digit day
			'%e'=>'j', // 1 digit day
			'%a'=>'D', // Abbr. Day Name
			'%A'=>'l', // Full Month Name
			'%o'=>'S', // Day Ordinal
			'%w'=>'w', // day of week (0=sun 6=sat)
			// hours
			'%H'=>'H', // 2 digit hours (24hr clock)
			'%#H'=>'h', // 1 digit hours (24hr clock)
			'%I'=>'G', // 2 digit hours (12hr clock)
			'%#I'=>'g', // 1 digit hours (12hr clock)
			'%p'=>'a', // AM or PM
			// minutes
			'%M'=>'i', // 2 digit minutes
			// seconds
			'%S'=>'s', // 2 digit seconds
			'%s'=>'U', // unix timestamp
			// timezone
			'%O'=>'p', // Time zone offset minutes
			'%Z'=>'e', // Time zone name
			'%G'=>'Z', // GMT Offset
			// shortcuts
			'%F'=>'Y-m-d',
			'%T'=>'H:i:s',
			'%X'=>'H:i:s',
			'%x'=>'m/d/y',
			'%D'=>'m/d/y',
			'%c'=>'D M j H:i:s Y',
			'%v'=>'j-M-Y',
			'%R'=>'H:i',
			'%r'=>'G:i:s p',
		);
		if (isset($map[$letter])) {
			return $map[$letter];
		}
		return $letter;
	}
	
	public static function textToRange($text, $format=null) {
		if (preg_match('/last (\d+) (second|minute|hour|day|week|month|year)s?/i', $text, $match)) {
			$begin = strtotime($match[1] . ' ' . $match[2] . ' ago');
			$end = time();
		}
		else {
			$text = strtolower($text);
			switch ($text) {
				case 'yesterday':
					$yesterdate = date('Y-m-d', strtotime('yesterday'));
					$begin = "$yesterdate 00:00:00";
					$end = "$yesterdate 23:59:59";
					break;
				case 'today':
					$today = date('Y-m-d');
					$begin =  "$today 00:00:00";
					$end = "$today 23:59:59";
					break;
				case 'last calendar month':
					$lastMonth = date('Y-m-01', strtotime('last month'));
					$firstOfMonth = self::create($lastMonth);
					$daysInMonth = $firstOfMonth->daysInMonth();
					$begin = $firstOfMonth->format('Y-m-d H:i:s');
					$end = substr($lastMonth,0,7) . "$daysInMonth 23:59:59";
					break;					
				case 'this calendar month':
					$lastMonth = date('Y-m-01');
					$firstOfMonth = self::create($lastMonth);
					$daysInMonth = $firstOfMonth->daysInMonth();
					$begin = $firstOfMonth->format('Y-m-d H:i:s');
					$end = substr($lastMonth,0,7) . "-$daysInMonth 23:59:59";
					break;					
				case 'last calendar year':
					$begin = date('Y-01-01 00:00:00', strtotime('last year'));
					$end = substr($begin,0,4) . "-12-31 23:59:59";
					break;					
				case 'this calendar year':
					$begin = date('Y-01-01 00:00:00');
					$end = substr($begin,0,4) . "-12-31 23:59:59";
					break;						
			}
		}
		return array(Quad_Date::create($begin), Quad_Date::create($end));
	}
	
	public function getCalendarMonth() {
		return new Quad_Date_Calendar_Month($this);
	}
	
	public function quadDebug() {
		return array(
			'ISO8601' => $this->format(self::ISO8601),
			'isValid' => $this->isValid
		);
	}
}